package cmich.cps.svn.utility;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Logger;

import cmich.cps.svn.utility.gui.Prompts;

/**
 * The Class UtilitySystem.
 *
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
public class UtilitySystem {

	/** The settings. */
	private Settings settings;

	/** The prompts. */
	private Prompts prompts;

	/** The Constant instance. */
	private static final UtilitySystem instance = new UtilitySystem();

	/** The Constant LOGGER. */
	private static final Logger LOGGER = Logger.getLogger(Settings.class.getName());

	/** The active repository. */
	private Repository activeRepo;

	/** The managed repositories. */
	private List<Repository> repositories = new ArrayList<Repository>();

	/**
	 * Instantiates a new utility system.
	 */
	private UtilitySystem() {}

	/**
	 * Gets the single instance of UtilitySystem.
	 *
	 * @return single instance of UtilitySystem
	 */
	public static UtilitySystem getInstance() {
		return instance;
	}

	/**
	 * Initializes variables.
	 */
	public void init() {
		LOGGER.finest("Initializing UtilitySystem.");
		settings = Settings.getInstance();
		prompts = Prompts.getInstance();
	}

	/**
	 * Startup.
	 */
	public void startup() {
		LOGGER.finer("Loading UtilitySystem.");
		if(this.repositories.size()==0) {
			addNewRepo();
		}
		else {
			setActiveRepo(this.repositories.get(0));
		}
		if(this.repositories.size()==0) {
			String message = "Utility cannot run without at least one repository.";
			LOGGER.warning(message);
			prompts.fatalError(message);
		}
	}

	/**
	 * Execute command.
	 *
	 * @param command the command
	 * @return the executed command's output, or the error message if an error occurred
	 */
	public String execCommand(String command) {
		String text = command;
		if(command.contains("password")) {
			text = text.replaceAll(settings.getPassword(),"");
		}
		LOGGER.finest("Input: " + text);
		String output = "";
		boolean timedOut = false;
		int seconds = 10; //Timeout in seconds
		int timeout = seconds * 1000;
		try {
			final long startTime = System.nanoTime();
			Process child;
			if(settings.isWindows()) {
				child = Runtime.getRuntime().exec(command);
			}
			else {
				String[] linuxCommand = parseArgs(command);
				child = Runtime.getRuntime().exec(linuxCommand);
			}
			Runnable readOut = new ReadOutput(child);
			Thread readerThread = new Thread(readOut);
			Timer timer = new Timer(true);
			try {
				readerThread.start();
				InterruptTimerTask interrupter = new InterruptTimerTask(Thread.currentThread());
				timer.schedule(interrupter, timeout);
				child.waitFor();
			} catch (InterruptedException e) {
				child.destroy();
				timedOut = true;
			} finally {
				LOGGER.finest("Command: " + text + " took " + (System.nanoTime()-startTime)/1000000000.0 + " seconds to execute.");
				timer.cancel();
				Thread.interrupted();
				readerThread.join();
				output = ((ReadOutput) readOut).getOutput();
			}
		} catch (IOException e) {
			if(e.toString().contains("Cannot run program \"svn\"")) {
				LOGGER.fine("Expected exception: " + e.toString());
			}
			else {
				LOGGER.warning(e.toString());
			}
			output = e.toString();
		}
		catch (InterruptedException e) {
			timedOut = true;
		}
		while(output.endsWith("\n") || output.endsWith("\r")) {
			output = output.substring(0, output.length()-1);
		}
		LOGGER.finest("Output: " + output);
		if(timedOut) {
			output = "Command timed out. Timeout=" + seconds;	
		}
		return output;
	}

	private String[] parseArgs(String command) {
		String[] newCommand = command.split(" ");
		ArrayList<String> commandList = new ArrayList<String>();
		String enclosedCommand = "";
		boolean enclosed = false;
		for(String string : newCommand) {
			if(string.startsWith("\"")  && !string.endsWith("\"")) {
				enclosed = true;
				enclosedCommand = string.replace("\"", "");
			}
			else if(enclosed) {
				if(string.endsWith("\"")) {
					enclosed = false;
					enclosedCommand = enclosedCommand + " " +  string.replace("\"", "");
					commandList.add(enclosedCommand);
					enclosedCommand = "";
				}
				else {
					enclosedCommand = enclosedCommand + " " +  string;
				}
			}
			else {
				commandList.add(string.replace("\"", ""));
			}
		}
		String[] newCommands = new String[commandList.size()];
		commandList.toArray(newCommands);
		return newCommands;
	}

	/**
	 * Checks out a repository.
	 *
	 * @param repo the repository to checkout
	 * @param repoLocalDir the local directory to checkout to
	 * @return true, if successful
	 */
	public boolean checkout(URI repo, File fileDir) {
		String result = svnCommand("checkout", repo + " " + settings.enclose(fileDir.getPath()));
		if(result.contains("Checked out revision")) {
			return true;
		}
		LOGGER.info("svn checkout failed, with result: " + result);
		return false;
	}

	/**
	 * Gets the property.
	 *
	 * @param property the property
	 * @param file the file
	 * @return the property
	 */
	public String getProperty(String property, String file) {
		String result = svnCommand("propget", property + " " + file);
		if(result.equals("")) {
			LOGGER.info("Property: " + property + " not set on " + file);
		}
		return result;
	}

	/**
	 * Executes a subversion command.
	 *
	 * @param command the command
	 * @param arguments the arguments
	 * @return the output of the subversion command, or the error message if an error occurred
	 */
	public String svnCommand(String command, String arguments) {
		String username = settings.getUsername();
		String password = settings.getPassword();
		String svnArgs = " --non-interactive --trust-server-cert";
		if(!settings.getAuthCache()) {
			command = command + " --no-auth-cache";
		}
		if(settings.isCygwin()) {
			arguments = arguments.replaceFirst("C:", "/cygdrive/c").replace("\\", "/");
		}
		String result = execCommand(settings.svn() + " " + command + svnArgs + username + password + " " + arguments);
		if(result.contains("authorization failed") || result.contains("401 Authorization Required")) {
			result = authorizationFailure(command, arguments, svnArgs);
		}
		if(result.contains("cygwin warning")) {
			settings.setCygwin();
			return svnCommand(command, arguments);
		}
		return result;
	}

	/**
	 * Handles authorization failures.
	 * 
	 * @param command the svn command
	 * @param arguments the arguments
	 * @param svnArgs the svn arguments
	 * @return the <code>command</code>'s output
	 */
	private String authorizationFailure(String command, String arguments, String svnArgs) {
		String username;
		String password;
		String result = "";
		boolean firstTime = true;
		String msg = "Authorization failure.";
		boolean failed = true;
		while(firstTime || (failed && prompts.confirm(msg + " Would you like to try again?"))) {
			firstTime = false;
			settings.setUserPass();
			username = settings.getUsername();
			password = settings.getPassword();
			result = execCommand(settings.svn() + " " + command + svnArgs + username + password + " " + arguments);
			if(!result.contains("authorization failed") || result.contains("401 Authorization Required")) {
				failed = false;
			}
		}
		return result;
	}

	/**
	 * Calls info on {@link #svnCommand(String, String)}
	 *
	 * @param file the file to get info about
	 * @return the output of the info command, or the error message if an error occurred
	 */
	public String info(String file) {
		String result = svnCommand("info", settings.enclose(file));
		return result;
	}

	/**
	 * Calls delete on {@link #svnCommand(String, String)}
	 *
	 * @param file the file to delete
	 * @return true, if successful
	 */
	public boolean delete(String file) {
		String result = svnCommand("delete", "--force " + settings.enclose(file));
		if(result.startsWith("D ")) {
			return true;
		}
		LOGGER.info("svn delete failed, with result: " + result);
		return false;
	}

	/**
	 * Sets the property on the directory, calls {@link #svnCommand(String, String)}.
	 *
	 * @param property the property to set
	 * @param value the value of the property
	 * @param dir the directory to set the property on
	 * @return true, if successful
	 */
	public boolean setProperty(String property, String value, String dir) {
		String result = svnCommand("propset", property + " \"" + value + "\" " + dir);
		if(result.startsWith("property '"+ property + "' set on '")) {
			LOGGER.config(result);
			return true;
		}
		LOGGER.info(result);
		return false;
	}

	/**
	 * Adds a new repository by checking it out to the {@link #localRepoDirectory}.
	 */
	public void addNewRepo() {
		URI repoURI = prompts.getRepoFromUser();
		if(repoURI!=null && !repoURI.toString().isEmpty()) {
			File repoLocalDir = setRepoLocalDir(repoURI);
			if(!checkout(repoURI, repoLocalDir)) {
				String message = "Unable to check out from repository at: " + repoURI;
				LOGGER.warning(message);
				prompts.nonFatalError(message);
			}
			else {
				Repository repo = new Repository(repoLocalDir);
				this.repositories.add(repo);
				LOGGER.fine("Added new repository: " + repo);
				setActiveRepo(repo);
			}
		}
	}

	/**
	 * Adds a repository to {@link #repositories}.
	 *
	 * @param repo The repository to add
	 */
	public void addRepo(Repository repo) {
		LOGGER.fine("Now managing repository: " + repo);
		this.repositories.add(repo);
	}

	/**
	 * Sets the local directory, within {@link #localRepoDirectory}, for a specific repository.
	 * 
	 * @param repo The URI of the repository to be set locally.
	 * @return The newly created directory for the repository.
	 */
	public File setRepoLocalDir(URI repo) {
		File repoLocalDir;
		try {
			String repoName = repo.toString().split("cmich.edu/")[1].split("/")[0];
			repoLocalDir = (new File(settings.getLocalRepoDirectory() + File.separator + repoName));
		}
		catch (ArrayIndexOutOfBoundsException e) {
			repoLocalDir = new File(settings.getLocalRepoDirectory() + File.separator + prompts.getInput("What would you like the local folder to be called?"));
		}
		if(!repoLocalDir.exists()) {
			if(!repoLocalDir.mkdir()) {
				String message = "Unable to create the directory: " + repoLocalDir.getAbsolutePath();
				LOGGER.warning(message);
				prompts.nonFatalError(message);
				repoLocalDir = setRepoLocalDir(repo);
			}
		}
		return repoLocalDir;
	}

	/**
	 * Gets the active repository.
	 *
	 * @return the active repository
	 */
	public Repository getActiveRepo() {
		return this.activeRepo;
	}

	/**
	 * Sets the active repository.
	 *
	 * @param repo the new active repository
	 */
	public void setActiveRepo(Repository repo) {
		LOGGER.fine("Active repository changing to " + repo);
		this.activeRepo = repo;
		repo.initialize();
	}

	/**
	 * Gets {@link #repositories}.
	 *
	 * @return the repositories
	 */
	public List<Repository> getRepositories() {
		return Collections.unmodifiableList(this.repositories);
	}	
}

/**
 * Simple {@link TimerTask} that interrupts the given waiting thread when the timer completes.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
class InterruptTimerTask extends TimerTask {

	private Thread thread;

	public InterruptTimerTask(Thread t)	{
		this.thread = t;
	}

	public void run() {
		thread.interrupt();
	}

}

/**
 * Reads the output of a child process.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
class ReadOutput implements Runnable {
	private String output = "";
	private Process child;
	public ReadOutput(Process child) {
		this.child = child;
	}
	@Override
	public void run() {
		try {
			BufferedReader in = new BufferedReader(new InputStreamReader(child.getInputStream()));
			String line = null;
			while ((line = in.readLine()) != null) {
				output = output + line + "\n";
			}
			in.close();

			BufferedReader error = new BufferedReader(new InputStreamReader(child.getErrorStream()));
			String errLine = null;
			while ((errLine = error.readLine()) != null) {
				output = output + errLine + "\n";
			}
			error.close();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
	public String getOutput() {
		return this.output;
	}
}
